import time
import datetime

from croniter import croniter
from validr import Compiler, T, Invalid


_parse_timedelta = Compiler().compile(T.timedelta.object)
_format_timedelta = Compiler().compile(T.timedelta.string.extended)


def parse_interval(expression: str) -> int:
    return _parse_timedelta(expression).total_seconds()


def format_interval(seconds: int) -> str:
    return _format_timedelta(datetime.timedelta(seconds=seconds))


class Timer:
    """
    cron:
        minute hour day month day_of_week
    interval:
        _d_h_m_s
    >>> UTC = datetime.timezone.utc
    >>> base = int(datetime.datetime(2020, 1, 1, tzinfo=UTC).timestamp())
    >>> t = Timer("1h")
    >>> t.next(base) - base
    3600
    >>> t = Timer("0 0 1 * *")
    >>> t.next(base) - base  # 31d
    2678400
    >>> repr(t)
    '<Timer 0 0 1 * *>'
    >>> Timer.is_valid("30m15s")
    True
    >>> Timer.is_valid("0 * * * *")
    True
    >>> Timer.is_valid("x 2 3 4 5")
    False
    """

    def __init__(self, expression, *, timezone=datetime.timezone.utc):
        self.timezone = timezone
        if isinstance(expression, datetime.timedelta):
            self.interval = int(expression.total_seconds())
        elif isinstance(expression, (int, float)):
            self.interval = int(expression)
        else:
            try:
                self.interval = parse_interval(expression)
            except (ValueError, TypeError, Invalid):
                if not croniter.is_valid(expression):
                    raise ValueError('invalid timer expression')
                else:
                    self.interval = None
                    self.expression = expression
        if self.interval is not None:
            self.expression = format_interval(self.interval)

    def __repr__(self):
        name = type(self).__name__
        return f'<{name} {self.expression}>'

    def __str__(self):
        return self.expression

    @classmethod
    def is_valid(cls, expression):
        try:
            cls(expression)
        except Exception:
            return False
        else:
            return True

    def next(self, base: int = None) -> int:
        """
        Get next alerm time
        """
        if base is None:
            base = int(time.time())
        if self.interval is None:
            base_dt = datetime.datetime.fromtimestamp(base, tz=self.timezone)
            it = croniter(self.expression, base_dt)
            return int(it.get_next())
        else:
            return int(base + self.interval)
